Skip to content

S13-05 React-Router

[TOC]

API

React-Router

  • <BrowserRouter>返回:,使用 HTML5 的 history API 来实现路由功能。
  • <HashRouter>返回:,使用 URL hash(即 URL 中的 # 字符)来实现路由功能。
  • <Switch>返回:,用于渲染与当前 URL 匹配的第一个 <Route><Redirect> 组件。当匹配到第一个符合条件的组件时,<Switch> 将停止匹配并呈现该组件。React Router 6.x中被 <Routes> 代替
  • <Routes>返回:,是一种React Router 6.x新的语法,用于定义路由配置。它是一个包含多个<Route><React.Fragment>元素的容器组件,每个<Route>元素都表示一个路由规则。
  • <Route>返回:,用于定义路由规则的组件。
    • 属性
    • path:``,表示路由规则的路径。
    • element,表示匹配到该路由规则时渲染的组件。注意:element的值是一个组件实例,如 <Home/>
  • <Link>返回:,用于定义导航链接的组件
    • 属性
    • tostring,表示链接的目标位置
    • replaceboolean,表示是否使用替换历史记录而不是添加一个新条目。默认为 false
    • stateobject,表示要传递给目标位置的状态。这个状态可以在目标位置的 location 对象中访问到。
  • <NavLink>返回:,用于定义导航链接的组件,它与 <Link> 的功能类似,但是在渲染时会自动添加一个 active 类名,以便高亮当前选中的链接。
    • 属性
    • tostring,表示链接的目标位置
    • style({isActive, isPending?}) => {},设置样式
    • className({isActive, isPending?}) => void,设置class
    • activeClassNamestring,表示选中链接时要添加的类名。默认为 active
    • activeStyleobject,表示选中链接时要应用的样式
    • isActivefunction,用于自定义激活链接的逻辑
  • <Navigate>返回:,用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中
    • 属性
    • tostring,表示要导航到的路径。
    • replaceboolean,表示是否使用替换历史记录而不是添加一个新条目。默认为 false
    • stateobject,表示要传递给目标位置的状态。这个状态可以在目标位置的 location 对象中访问到。
  • <Outlet>返回:,是一个新的组件,用于在路由组件中渲染子路由。它充当了一个占位符,用于渲染嵌套路由的内容。
  • useNavigate返回:
  • useParams返回:
  • useLocation返回:
  • useRoutes返回:

React

  • React.lazy()返回:

认识React-Router

认识前端路由

路由其实是网络工程中的一个术语:

  • 在架构一个网络时,非常重要的两个设备就是路由器和交换机。

  • 当然,目前在我们生活中路由器也是越来越被大家所熟知,因为我们生活中都会用到路由器:

  • 事实上,路由器主要维护的是一个映射表;

  • 映射表会决定数据的流向;

路由的概念在软件工程中出现,最早是在后端路由中实现的,原因是web的发展主要经历了这样一些阶段:

  • 后端路由阶段;

  • 前后端分离阶段;

  • 单页面富应用(SPA);

后端路由阶段

早期的网站开发整个HTML页面是由服务器来渲染的.

  • 服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示.

但是, 一个网站, 这么多页面服务器如何处理呢?

  • 一个页面有自己对应的网址, 也就是URL;

  • URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理;

  • Controller进行各种处理, 最终生成HTML或者数据, 返回给前端.

上面的这种操作, 就是后端路由

  • 当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端.

  • 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.

后端路由的缺点:

  • 一种情况是整个页面的模块由后端人员来编写和维护的;

  • 另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码;

  • 而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情;

前后端分离阶段

前端渲染的理解:

  • 每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染;

  • 需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件;

  • 同时可以看到,和之前的后端路由不同,这时后端只是负责提供API了;

前后端分离阶段:

  • 随着Ajax的出现, 有了前后端分离的开发模式;

  • 后端只提供API来返回数据,前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中;

  • 这样做最大的优点就是前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上;

  • 并且当移动端(iOS/Android)出现后,后端不需要进行任何处理,依然使用之前的一套API即可;

  • 目前比较少的网站采用这种模式开发;

单页面富应用阶段:

  • 其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.

  • 也就是前端来维护一套路由规则.

前端路由的核心是什么呢?改变URL,但是页面不进行整体的刷新。

URL的hash

前端路由是如何做到URL和内容进行映射呢?监听URL的改变

URL的hash

  • URL的hash也就是锚点(#), 本质上是改变window.location的href属性

  • 我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;

image-20230404113715957

image-20230404113728906

hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷有一个#,显得不像一个真实的路径

HTML5的History

history接口是HTML5新增的, 它有六种模式改变URL而不刷新页面:

  • replaceState:替换原来的路径;

  • pushState:使用新的路径;

  • popState:路径的回退;

  • go:向前或向后改变路径;

  • forward:向前改变路径;

  • back:向后改变路径;

image-20230404113743430

image-20230404113753940

认识react-router

目前前端流行的三大框架, 都有自己的路由实现:

  • Angular的ngRouter

  • React的ReactRouter

  • Vue的vue-router

React Router在最近两年版本更新的较快,并且在最新的React Router6.x版本中发生了较大的变化。

  • 目前React Router6.x已经非常稳定,我们可以放心的使用;

Router-基本使用

安装React Router

  • 安装时,我们选择react-router-dom

  • react-router会包含一些react-native的内容,web开发并不需要;

sh
npm install react-router-dom

Router的基本使用

react-router最主要的API是给我们提供的一些组件:

  • <BrowserRouter>返回:,使用 HTML5 的 history API 来实现路由功能。
  • <HashRouter>返回:,使用 URL hash(即 URL 中的 # 字符)来实现路由功能。

BrowserRouterHashRouter

  • Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;

  • BrowserRouter使用history模式

  • HashRouter使用hash模式

js
// index.js
  import { HashRouter } from 'react-router-dom'

  const root = ReactDOM.createRoot(document.getElementById('root'))
  root.render(
+    <HashRouter>
      <App />
+    </HashRouter>
  )

路由映射配置

  • <Switch>返回:,用于渲染与当前 URL 匹配的第一个 <Route><Redirect> 组件。当匹配到第一个符合条件的组件时,<Switch> 将停止匹配并呈现该组件。React Router 6.x中被 <Routes> 代替
  • <Routes>返回:,是一种React Router 6.x新的语法,用于定义路由配置。它是一个包含多个<Route><React.Fragment>元素的容器组件,每个<Route>元素都表示一个路由规则。
  • <Route>返回:,用于定义路由规则的组件。
    • 属性
    • path:``,表示路由规则的路径。
    • element,表示匹配到该路由规则时渲染的组件。注意:element的值是一个组件实例,如 <Home/>

Routes:包裹所有的Route,在其中匹配一个路由

  • Router5.x使用的是Switch组件

Route:Route用于路径的匹配;

  • path属性:用于设置匹配到的路径;

  • element属性:设置匹配到路径后,渲染的组件;

    • Router5.x使用的是component属性
  • exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;

    • Router6.x不再支持该属性

App.jsx

js
export class App extends PureComponent {
  render() {
    return (
      <div>
        <div className="header">Header</div>
        <hr />
        <div className="content">
+          <Routes>
+            <Route path='/home' element={<Home/>}/>
+            <Route path='/profile' element={<Profile/>}/>
+          </Routes>
        </div>
        <hr />
        <div className="footer">Footer</div>
      </div>
    )
  }
}

image-20230417165959479

路由配置和跳转

  • <Link>返回:,用于定义导航链接的组件
    • 属性
    • tostring,表示链接的目标位置
    • replaceboolean,表示是否使用替换历史记录而不是添加一个新条目。默认为 false
    • stateobject,表示要传递给目标位置的状态。这个状态可以在目标位置的 location 对象中访问到。

LinkNavLink:**

  • 通常路径的跳转是使用Link组件,最终会被渲染成a元素;

  • NavLink是在Link基础之上增加了一些样式属性(后续学习);

  • to属性:Link中最重要的属性,用于设置跳转到的路径;

image-20230404114032802

image-20230404114039269

<NavLink>返回:,用于定义导航链接的组件,它与 <Link> 的功能类似,但是在渲染时会自动添加一个 active 类名,以便高亮当前选中的链接。

  • 属性
  • tostring,表示链接的目标位置
  • style({isActive, isPending?}) => {},设置样式
  • className({isActive, isPending?}) => void,设置class
  • activeClassNamestring,表示选中链接时要添加的类名。默认为 active
  • activeStyleobject,表示选中链接时要应用的样式
  • isActivefunction,用于自定义激活链接的逻辑

需求:路径选中时,对应的a元素变为红色

这个时候,我们要使用NavLink组件来替代Link组件:

  • style:传入函数,函数接受一个对象,包含isActive属性

    js
      <NavLink
        to="/detail"
        style={({ isActive }) => ({ backgroundColor: isActive ? '#ddd' : '' })}
      >
        详情
      </NavLink>
  • className:传入函数,函数接受一个对象,包含isActive属性

    js
    {/* 1. 普通添加class */}
    <NavLink to="/category" className={({isActive}) => isActive ? 'mractive' : ''}>分类</NavLink>
    {/* 2. 使用classnames库 */}
    <NavLink to="/home" className={({isActive}) => classNames({mractive: isActive})}>首页</NavLink>
  • activeClassName

    • 事实上在默认匹配成功时,NavLink就会添加上一个动态的active class;
    js
    <div className="footer">
        <NavLink to="/home">首页</NavLink>  
        <NavLink to="/profile">我的</NavLink>  
    </div>

    image-20230417172148535

    • 所以我们也可以直接编写样式

当然,如果你担心这个class在其他地方被使用了,出现样式的层叠,也可以自定义class

<Navigate>返回:,用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中

  • 属性
  • tostring,表示要导航到的路径。
  • replaceboolean,表示是否使用替换历史记录而不是添加一个新条目。默认为 false
  • stateobject,表示要传递给目标位置的状态。这个状态可以在目标位置的 location 对象中访问到。

Navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:

我们这里使用这样的一个案例:

  • 用户跳转到Profile界面;

  • 但是在Profile界面有一个isLogin用于记录用户是否登录:

    • true:那么显示用户的名称;

    • false:直接重定向到登录界面;

image-20230404114121101

image-20230404114126375

示例:点击登录跳转到Home页

js
export class Profile extends PureComponent {
  constructor() {
    super()
    this.state = {
+      isLogin: false
    }
  }
  render() {
    const { isLogin } = this.state
    return (
      <div>
        <h3>Profile</h3>
        {/* 
          功能:通过isLogin判断是否登录,
            如果登录则跳转到首页,
            如果没有登录则留在本页,并且在本页提供一个登录按钮,点击实现登录
        */}
        {
+          isLogin ? <Navigate to='/home'/> : <button onClick={e => this.setState({isLogin: true})}>登录</button>
        }
      </div>
    )
  }
}

示例:匹配到/时直接跳转到/home页面

js
  <Routes>
+    <Route path='/' element={<Navigate to='/home'/>} />
    <Route path="/home" element={<Home />} />
    <Route path="/profile" element={<Profile />} />
    <Route path="/detail" element={<Detail />} />
    <Route path="/category" element={<Category />} />
  </Routes>

Not Found页面配置

如果用户随意输入一个地址,该地址无法匹配,那么在路由匹配的位置将什么内容都不显示。

很多时候,我们希望在这种情况下,让用户看到一个Not Found的页面

这个过程非常简单:

  • 开发一个Not Found页面;

  • 配置对应的Route,并且*设置path为**即可;

js
  <Routes>
    <Route path='/' element={<Navigate to='/home'/>} />
    <Route path="/home" element={<Home />} />
    <Route path="/profile" element={<Profile />} />
    <Route path="/detail" element={<Detail />} />
    <Route path="/category" element={<Category />} />
+    <Route path='*' element={<NotFound />} />
  </Routes>

image-20230404114159302

Router-路由嵌套

  • <Outlet>返回:,是一个新的组件,用于在路由组件中渲染子路由。它充当了一个占位符,用于渲染嵌套路由的内容。

在开发中,路由之间是存在嵌套关系的。

这里我们假设Home页面中有两个页面内容:

  • 推荐列表和排行榜列表;

  • 点击不同的链接可以跳转到不同的地方,显示不同的内容;

Outlet 组件用于在父路由元素中作为子路由的占位元素

App.jsx

js

  <Routes>
    <Route path='/' element={<Navigate to='/home'/>} />
+    <Route path="/home" element={<Home />}>
+      <Route path='/home/recommend' element={<HomeRecommend />}/>
+      <Route path='/home/rank' element={<HomeRank />}/>
+    </Route>
    <Route path="/profile" element={<Profile />} />
    <Route path="/detail" element={<Detail />} />
    <Route path="/category" element={<Category />} />
    <Route path='*' element={<NotFound />} />
  </Routes>

Home.jsx

js
  <Link to='/home/recommend'>推荐榜 </Link>
  <Link to='/home/rank'>排行榜</Link>
+  <Outlet />

image-20230404114217898

Router-手动路由跳转

目前我们实现的跳转主要是通过Link或者NavLink进行跳转的,实际上我们也可以通过JavaScript代码进行跳转。

  • 我们知道Navigate组件是可以进行路由跳转的,但是依然是组件的方式。

  • 如果我们希望通过JavaScript代码逻辑进行跳转(比如点击了一个button),那么就需要获取到navigate对象。

在Router6.x版本之后,代码类的API都迁移到了hooks的写法:

  • 如果我们希望进行代码跳转,需要通过useNavigate的Hook获取到navigate对象进行操作;

  • 那么如果是一个函数式组件,我们可以直接调用,但是如果是一个类组件呢?

image-20230404114303869

使用useNavigate的方法:

1、修改成函数组件

js
  import React from 'react'
  import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'

  import Home from './cpns/Home'
  import Profile from './cpns/Profile'
  import NotFound from './cpns/NotFound'

+  export function App(props) {
    // 1. 调用useNavigate钩子,返回方法
+    const navigate = useNavigate()
    return (
      <div>
        <div className="header">
          {/* 2. 使用navigate方法,实现跳转 */}
+          <button onClick={e => navigate('/home')}>首页</button>
+          <button onClick={e => navigate('/profile')}>我的</button>
        </div>
        <hr />
        <div className="content">
          <Routes>
            <Route path="/" element={<Navigate to="/home" />} />
            <Route path="/home" element={<Home />} />
            <Route path="/profile" element={<Profile />} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </div>
        <hr />
        <div className="footer">Footer</div>
      </div>
    )
  }

  export default App

2、对类组件使用高阶组件包裹

hoc/withRouter.js

js
  import { useNavigate } from "react-router-dom"

  function withRouter(Cpn) {
    return function(props) {
      const navigate = useNavigate()
      const router = { navigate }
      return <Cpn {...props} router={router}/>
    }
  }

  export default withRouter

App.jsx

js
import React, { PureComponent } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'

import Home from './cpns/Home'
import Profile from './cpns/Profile'
import NotFound from './cpns/NotFound'
import withRouter from './hoc/withRouter'

+ export class App extends PureComponent {
  render() {
    // 2. 解构出router对象中的navigate方法
+    const { router } = this.props
+    const { navigate } = router
    return (
      <div>
        <div className="header">
          {/* 3. 使用navigate方法,实现跳转 */}
+          <button onClick={(e) => navigate('/home')}>首页</button>
+          <button onClick={(e) => navigate('/profile')}>我的</button>
        </div>
        <hr />
        <div className="content">
          <Routes>
            <Route path="/" element={<Navigate to="/home" />} />
            <Route path="/home" element={<Home />} />
            <Route path="/profile" element={<Profile />} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </div>
        <hr />
        <div className="footer">Footer</div>
      </div>
    )
  }
}
  // 1. 使用高阶组件withRouter包裹App
+ export default withRouter(App)

Router-参数传递

传递参数有二种方式:

  • 动态路由参数

  • 查询字符串参数

1、动态路由参数

动态路由的概念指的是路由中的路径并不会固定:

  • 比如/detail的path对应一个组件Detail;

  • 如果我们将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示;

  • 这个匹配规则,我们就称之为动态路由;

  • 通常情况下,使用动态路由可以为路由传递参数。

跳转时传递参数

1、路由

js
  <Routes>
    <Route path="/" element={<Navigate to="/home" />} />
    {/* 1. 路由传参-动态路由 */}
+    <Route path="/home/:id" element={<Home />} />
    <Route path="/profile" element={<Profile />} />
    <Route path="*" element={<NotFound />} />
  </Routes>

2、跳转时传参

js
  {/* 2. 使用navigate方法,实现跳转 */}
+  <button onClick={(e) => navigate('/home/123')}>首页</button>
+  <Link to="/profile/456">我的</Link>

3、由于类组件不能使用useParams,所以必须对该类组件进行增强

js
  import { useNavigate, useParams } from "react-router-dom"

  function withRouter(Cpn) {
    return function(props) {
      // 路由跳转
      const navigate = useNavigate()
      // 路由传参-动态路由
+      const params = useParams()
      const router = { navigate, params }
      return <Cpn {...props} router={router}/>
    }
  }

  export default withRouter

4、跳转后获取参数

js
import React, { PureComponent } from 'react'
import withRouter from '../hoc/withRouter'

export class Home extends PureComponent {

  render() {
    const { router } = this.props
+    const { params } = router
    console.log('Home: ', params)
    return (
      <div>
        <h3>Home</h3>
+        <div>id: {params.id}</div>
      </div>
    )
  }
}

+ export default withRouter(Home)

2、查询字符串参数

  • useLocation:``,
  • useSearchParams:``,

1、路由

js
  <Routes>
    <Route path="/" element={<Navigate to="/home" />} />
    {/* 1. 路由传参-动态路由 */}
    <Route path="/home/:id" element={<Home />} />
    <Route path="/profile" element={<Profile />} />
    {/* 2. 路由传参-查询字符串 */}
+    <Route path='/about' element={<About />}/>
    <Route path="*" element={<NotFound />} />
  </Routes>

2、传递参数

js
<Link to="/about?name=Tom&age=20">关于</Link>

3、由于类组件不能使用useParams,所以必须对该类组件进行增强

方式一:useSearchParams()

方式二:useLocation()

js
  import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"

  function withRouter(Cpn) {
    return function(props) {
      // 路由跳转
      const navigate = useNavigate()

      // 路由传参-动态路由
      const params = useParams()

      // 路由传参-查询字符串-useSearchParams
+      const [ searchParams ] = useSearchParams()
+      const query = Object.fromEntries(searchParams.entries())
      
      // 路由传参-查询字符串-useLocation
+      const { search } = useLocation()
+      const searchEntries = new URLSearchParams(search)
+      const searchObj = Object.fromEntries(searchEntries)

      const router = { navigate, params, query, searchObj }
      return <Cpn {...props} router={router}/>
    }
  }

  export default withRouter

4、获取参数

js
import React, { PureComponent } from 'react'
import withRouter from '../hoc/withRouter'

export class About extends PureComponent {
  render() {
    const { router } = this.props
+    const { query, searchObj } = router
    return (
      <div>
        <div>About</div>
+        <div>name: {query.name}, age: {query.age}</div>
+        <div>name-{searchObj.name}, age-{searchObj.age}</div>
      </div>
    )
  }
}

+ export default withRouter(About)

Router-配置文件

目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。

但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:

  • 在早期的时候,Router并且没有提供相关的API,我们需要借助于react-router-config完成;

  • 在Router6.x中,为我们提供了useRoutes API可以完成相关的配置;

配置过程

1、创建 router/index.js

通过 useRoutes 将配置信息生成为Route组件,导出一个BaseRoute 的函数组件

js
import { Navigate, useRoutes } from "react-router-dom";
export default function BaseRoute() {
  return useRoutes(
    [
      { path: '/', element: <Navigate to='/home'/> },
      {
        path: '/home', 
        element: <Home />, 
        children: [
          { path: '/home/recommend', element: <HomeRecommend /> },
          { path: '/home/banner', element: <HomeBanner /> }
        ]
      },
      { path: '/profile', element: <Profile /> },
      { path: '/about', element: <About /> },
      { path: '*', element: <NotFound /> }
    ]
  )
}

2、在App.jsx 中使用该组件

js
  <div className="content">
+    <BaseRoute />
  </div>

注意: 这里不能在router/index.js 中导出routes ,然后在App.jsx 中使用useRoutes 生成Route组件,会报错

sh
React Hook "useRoutes" cannot be called in a class component

注意: 在路由配置中如果该路由下面有子路由(children),那么path不能写成/home/:id这样,只能是/home

3、如果我们对某些组件进行了异步加载(懒加载),那么需要使用Suspense进行包裹,实现分包

js

image-20230404114357651

image-20230404114406904

懒加载

React.lazy() 是 React 16.6 新增的 API,它实现了一种动态加载组件的方式,可以延迟组件的加载到真正需要渲染的时候再进行加载,从而优化页面加载速度。其语法如下:

js
const MyComponent = React.lazy(() => import('./MyComponent'));

其中 import() 函数返回一个 Promise 对象,表示 MyComponent 组件的代码将在被第一次渲染时动态地加载。

在使用React.lazy()时,需要注意几个方面:

  1. React.lazy() 只能用于函数式组件,不能用于 class 组件;
  2. 必须使用 React.Suspense 组件来包装懒加载组件,以防止在加载完成之前渲染出错;
  3. 在开发环境中,React.lazy() 需要与 babel 插件*@babel/plugin-syntax-dynamic-import* 配合使用。

以下是一个示例,展示了如何使用React.lazy()实现懒加载:

jsx
import React, { lazy, Suspense } from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));
function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

在上面的代码中,我们使用 React.lazy() 动态加载了一个组件,然后使用 <Suspense> 组件包裹起来,设置了一个 fallback 属性,表示在组件加载完成前显示的内容,这里设置为 "Loading..."。这样确保了即使组件还未加载完成,也不会影响整个页面的渲染。

另外值得一提的是,React.lazy()并不会影响应用的事件处理逻辑,因为它并不是异步地获取数据,而是关于代码的懒加载。